import dataclasses
import os
import pathlib

import jinja2

from chaotic import cpp_format
from chaotic import jinja_env
from chaotic.back.cpp import renderer as cpp_renderer
from chaotic_openapi.back.cpp_client import types

PARENT_DIR = os.path.dirname(__file__)

TEMPLATE_NAMES = [
    'client_fwd.hpp',
    'client.hpp',
    'client.cpp',
    'client_impl.hpp',
    'client_impl.cpp',
    'qos.hpp',
    'component.hpp',
    'component.cpp',
    'requests.hpp',
    'requests.cpp',
    'responses.hpp',
    'responses.cpp',
    'exceptions.hpp',
    'exceptions.cpp',
]

THIS_FILE_IS_AUTOGENERATED = "/* THIS FILE IS AUTOGENERATED, DON'T EDIT! */\n"


@dataclasses.dataclass
class Context:
    generate_path: pathlib.Path
    clang_format_bin: str
    uservices_library_tvm_guard_hack: bool


@dataclasses.dataclass
class CppOutput:
    rel_path: str
    content: str

    @staticmethod
    def save(outputs: list['CppOutput'], prefix: str) -> None:
        for output in outputs:
            path = os.path.join(prefix, output.rel_path)
            content = THIS_FILE_IS_AUTOGENERATED + output.content
            content = content.strip() + '\n'

            os.makedirs(os.path.dirname(path), exist_ok=True)
            with open(path, 'w') as ofile:
                ofile.write(content)


def cpp_comment(string: str) -> str:
    comment = string.replace('\r', '').strip().replace('\n', '\n/// ')
    if not comment:
        return ''

    # Not using .capitalize() to avoid lowercasing the second sentence.
    return '/// ' + comment[0].upper() + comment[1:]


def make_env() -> jinja2.Environment:
    env = jinja_env.make_env(
        'chaotic-openapi/chaotic_openapi/back/cpp_client',
        os.path.join(PARENT_DIR),
    )
    env.globals['list'] = list
    env.globals['enumerate'] = enumerate
    env.globals['str'] = str
    env.filters['cpp_comment'] = cpp_comment

    return env


JINJA_ENV = make_env()


def render(spec: types.ClientSpec, context: Context) -> list[CppOutput]:
    assert '-' not in spec.cpp_namespace
    env = {
        'spec': spec,
        'namespace': spec.cpp_namespace,
        'name': spec.client_name,
        'config': spec.dynamic_config,
        'base_url': 'http://example.com',  # TODO
        'operations': spec.operations,
        'tvm_hack': context.uservices_library_tvm_guard_hack,
    }

    # client* files
    output = []
    for name in TEMPLATE_NAMES:
        tpl = JINJA_ENV.get_template(f'templates/{name}.jinja')
        pp = tpl.render(**env)
        pp = cpp_format.format_pp(pp, binary=context.clang_format_bin)

        if name.endswith('.hpp'):
            rel_path = f'include/clients/{spec.client_name}/{name}'
        else:
            rel_path = f'src/clients/{spec.client_name}/{name}'

        output.append(CppOutput(rel_path=rel_path, content=pp))

    vfilepath_map = {}
    for cpp_type in spec.extract_cpp_types().values():
        assert cpp_type.json_schema
        filepath = cpp_type.json_schema.source_location().filepath
        vfilepath_map[filepath] = 'clients/{}/{}'.format(
            spec.client_name,
            filepath,
        )

    # C++ types files
    r = cpp_renderer.OneToOneFileRenderer(
        relative_to='',
        vfilepath_to_relfilepath=vfilepath_map,
        clang_format_bin=context.clang_format_bin,
        generate_serializer=True,
    )
    cpp_outputs = r.render(
        spec.extract_cpp_types(),
        local_pair_header=False,
    )
    for cpp_output in cpp_outputs:
        for file in cpp_output.files:
            output.append(
                CppOutput(
                    rel_path=os.path.join(file.subdir, cpp_output.filepath_wo_ext + file.ext),
                    content=file.content,
                ),
            )

    return output
